using System;
using System.IO;
using System.Collections;
using System.Text;

namespace LumiSoft.Net.Mime
{
	/// <summary>
	/// Rfc 2822 Mime Entity.
	/// </summary>
	public class MimeEntity
	{
		private HeaderFieldCollection m_pHeader           = null;
		private MimeEntity            m_pParentEntity     = null;
		private MimeEntityCollection  m_pChildEntities    = null;
		private byte[]                m_EncodedData       = null;
		private Hashtable             m_pHeaderFieldCache = null;

		/// <summary>
		/// Default constructor.
		/// </summary>
		public MimeEntity()
		{
			m_pHeader = new HeaderFieldCollection();
			m_pChildEntities = new MimeEntityCollection(this);
			m_pHeaderFieldCache = new Hashtable();
		}


		#region method Parse

		/// <summary>
		/// Parses mime entity from stream.
		/// </summary>
		/// <param name="stream">Data stream from where to read data.</param>
		/// <param name="toBoundary">Entity data is readed to specified boundary.</param>
		/// <returns>Returns false if last entity. Returns true for mulipart entity, if there are more entities.</returns>
		internal bool Parse(Stream stream,String toBoundary)
		{
			// Clear header fields
			m_pHeader.Clear();
			m_pHeaderFieldCache.Clear();

			// Parse header
			m_pHeader.Parse(stream);

			// Parse entity and child entities if any (Conent-Type: multipart/xxx...)

			// Multipart entity
			if((this.ContentType & MediaType_enum.Multipart) != 0){
				// There must be be boundary ID (rfc 1341 7.2.1  The Content-Type field for multipart entities requires one parameter, "boundary", which is used to specify the encapsulation boundary.)
				string boundaryID = this.ContentType_Boundary;
				if(boundaryID == null){
					// This is invalid message, just skip this mime entity
				}
				else{
					// There is one or more mime entities
				
					// Find first boundary start position
					StreamLineReader reader = new StreamLineReader(stream);
                    reader.CRLF_LinesOnly = false;
					byte[] lineData = reader.ReadLine();					
					while(lineData != null){
						string line = System.Text.Encoding.Default.GetString(lineData);
						if(line.StartsWith("--" + boundaryID)){
							break;
						}
						
						lineData = reader.ReadLine();
					}
					// This is invalid entity, boundary start not found. Skip that entity.
					if(lineData == null){
						return false;
					}
					
					// Start parsing child entities ot this entity
					while(true){					
						// Parse and add child entity
						MimeEntity childEntity = new MimeEntity();					
						this.ChildEntities.Add(childEntity);
						
						// This is last entity, stop parsing
						if(childEntity.Parse(stream,boundaryID) == false){
							break;
						}
						// else{
						// There are more entities, parse them
					}
					
					// This entity is child of mulipart entity.
					// All this entity child entities are parsed,
					// we need to move stream position to next entity start.
					if(toBoundary != null && toBoundary.Length > 0){
						lineData = reader.ReadLine();					
						while(lineData != null){
							String line = System.Text.Encoding.Default.GetString(lineData);
							if(line.StartsWith("--" + toBoundary)){
								break;
							}
							
							lineData = reader.ReadLine();
						}
						
						// Invalid boundary end, there can't be more entities 
						if(lineData == null){
							return false;
						}
						
						// See if last boundary or there is more. Last boundary ends with --
						if(System.Text.Encoding.Default.GetString(lineData).EndsWith(toBoundary + "--")){
							return false; 
						}
						// else{
						// There are more entities					
						return true;
					}
				}
			}
			// Singlepart entity
			else{
				// Boundary is specified, read data to specified boundary.
				if(toBoundary != null && toBoundary.Length > 0){
					MemoryStream entityData = new MemoryStream();
					StreamLineReader reader = new StreamLineReader(stream);
                    reader.CRLF_LinesOnly = false;
					byte[] lineData = reader.ReadLine();
					bool firstLine = true;
					while(lineData != null){
						string line = System.Text.Encoding.Default.GetString(lineData);
						if(line.StartsWith("--" + toBoundary)){
							break;
						}

						// Add previous line CRLF. 
						// We must do so because we need to skip last CRLF before boundary end, this is part of boundary.
						// For example:
						//	--boundary
						//  header
						//	
						//  data<CRLF> ----> this CRLF is part of boundary and must not added to boundary data !
						//	--boundary--
						if(!firstLine){
							entityData.Write(new byte[]{(byte)'\r',(byte)'\n'},0,2);
						}
						else{
							firstLine = false;
						}
						
						// Write line to buffer
						entityData.Write(lineData,0,lineData.Length);
										
						lineData = reader.ReadLine();
					}
				
					// This is invalid entity, unexpected end of entity, no boundary closing tag.
					if(lineData == null){
                        // Just return data what was readed.
                        m_EncodedData = entityData.ToArray();
                        return false;

                        /*
						if(this.ParentEntity != null){
							this.ParentEntity.ChildEntities.Remove(this);
						}*/
					}
					
					m_EncodedData = entityData.ToArray();
					
					// See if last boundary or there is more. Last boundary ends with --
					if(System.Text.Encoding.Default.GetString(lineData).EndsWith(toBoundary + "--")){
						return false; 
					}
					
					return true;
				}
				// Boundary isn't specified, read data to the stream end. 
				else{
                    m_EncodedData = new byte[stream.Length - stream.Position];
					stream.Read(m_EncodedData,0,m_EncodedData.Length);
				}
			}
			
			return false;
		}

		#endregion


		#region method ToStream

		/// <summary>
		/// Stores mime entity and it's child entities to specified stream.
		/// </summary>
		/// <param name="storeStream">Stream where to store mime entity.</param>
		public void ToStream(Stream storeStream)
		{			
			// Write headers
			byte[] data = System.Text.Encoding.Default.GetBytes(FoldHeader(this.HeaderString));
			storeStream.Write(data,0,data.Length);

			// If multipart entity, write child entities.(multipart entity don't contain data, it contains nested entities )
			if((this.ContentType & MediaType_enum.Multipart) != 0){
				string boundary = this.ContentType_Boundary;			
				foreach(MimeEntity entity in this.ChildEntities){
					// Write boundary start. Syntax: <CRLF>--BoundaryID<CRLF>
					data = System.Text.Encoding.Default.GetBytes("\r\n--" + boundary + "\r\n");
					storeStream.Write(data,0,data.Length);

					// Force child entity to store itself
                    entity.ToStream(storeStream);
				}

				// Write boundaries end Syntax: <CRLF>--BoundaryID--<CRLF>
				data = System.Text.Encoding.Default.GetBytes("\r\n--" + boundary + "--\r\n");
				storeStream.Write(data,0,data.Length);
			}
			// If singlepart (text,image,audio,video,message, ...), write entity data.
			else{				
				// Write blank line between headers and content
				storeStream.Write(new byte[]{(byte)'\r',(byte)'\n'},0,2);

				if(this.DataEncoded != null){
					storeStream.Write(this.DataEncoded,0,this.DataEncoded.Length);
				}				
			}
		}

		#endregion


		#region method DataToFile

		/// <summary>
		/// Saves this.Data property value to specified file.
		/// </summary>
		/// <param name="fileName">File name where to store data.</param>
		public void DataToFile(string fileName)
		{
			using(FileStream fs = File.Create(fileName)){
				DataToStream(fs);
			}
		}

		#endregion

		#region method DataToStream

		/// <summary>
		/// Saves this.Data property value to specified stream.
		/// </summary>
		/// <param name="stream">Stream where to store data.</param>
		public void DataToStream(Stream stream)
		{
			byte[] data = this.Data;
			stream.Write(data,0,data.Length);
		}

		#endregion

		#region method DataFromFile

		/// <summary>
		/// Loads MimeEntity.Data property from file.
		/// </summary>
		/// <param name="fileName">File name.</param>
		public void DataFromFile(string fileName)
		{
			using(FileStream fs = File.OpenRead(fileName)){
				DataFromStream(fs);
			}
		}

		#endregion

		#region method DataFromStream

		/// <summary>
		/// Loads MimeEntity.Data property from specified stream. Note: reading starts from current position and stream isn't closed.
		/// </summary>
		/// <param name="stream">Data stream.</param>
		public void DataFromStream(Stream stream)
		{
			byte[] data = new byte[stream.Length];
			stream.Read(data,0,(int)stream.Length);

			this.Data = data;
		}

		#endregion


		#region method EncodeData

		/// <summary>
		/// Encodes data with specified content transfer encoding.
		/// </summary>
		/// <param name="data">Data to encode.</param>
		/// <param name="encoding">Encoding with what to encode data.</param>
		private byte[] EncodeData(byte[] data,ContentTransferEncoding_enum encoding)
		{
			// Allow only known Content-Transfer-Encoding (ContentTransferEncoding_enum value),
            // otherwise we don't know how to encode data.
			if(encoding == ContentTransferEncoding_enum.NotSpecified){
				throw new Exception("Please specify Content-Transfer-Encoding first !");
			}
			if(encoding == ContentTransferEncoding_enum.Unknown){
				throw new Exception("Not supported Content-Transfer-Encoding. If it's your custom encoding, encode data yourself and set it with DataEncoded property !");
			}
				
			if(encoding == ContentTransferEncoding_enum.Base64){
				return Core.Base64Encode(data);
			}
			else if(encoding == ContentTransferEncoding_enum.QuotedPrintable){
				return Core.QuotedPrintableEncode(data);
			}
			else{
				return data;
			}
		}

		#endregion

		#region method FoldHeader

		/// <summary>
		/// Folds header.
		/// </summary>
		/// <param name="header">Header string.</param>
		/// <returns></returns>
		private string FoldHeader(string header)
		{			
			/* Rfc 2822 2.2.3 Long Header Fields
				Each header field is logically a single line of characters comprising
				the field name, the colon, and the field body.  For convenience
				however, and to deal with the 998/78 character limitations per line,
				the field body portion of a header field can be split into a multiple
				line representation; this is called "folding".  The general rule is
				imply WSP characters), a CRLF may be inserted before any WSP.  For
				example, the header field:

					Subject: This is a test

					can be represented as:

							Subject: This
								is a test
			*/

			// Just fold header fields what contain <TAB>

			StringBuilder retVal = new StringBuilder();
			
			header = header.Replace("\r\n","\n");
			string[] headerLines = header.Split('\n');		
			foreach(string headerLine in headerLines){
				// Folding is needed
				if(headerLine.IndexOf('\t') > -1){
					retVal.Append(headerLine.Replace("\t","\r\n\t") + "\r\n");
				}
				// No folding needed, just write back header line
				else{
					retVal.Append(headerLine + "\r\n");
				}
			}
			// Split splits last line <CRLF> to element, but we don't need it 
			if(retVal.Length > 1){
				return retVal.ToString(0,retVal.Length - 2);
			}
			else{
				return retVal.ToString();
			}
		}

		#endregion
		

		#region Properties Implementation

		/// <summary>
		/// Gets message header.
		/// </summary>
		public HeaderFieldCollection Header
		{
			get{ return m_pHeader; }
		}

		/// <summary>
		/// Gets header as RFC 2822 message headers.
		/// </summary>
		public string HeaderString
		{			
			get{ return m_pHeader.ToHeaderString("utf-8"); }
		}

		/// <summary>
		/// Gets parent entity of this entity. If this entity is top level, then this property returns null.
		/// </summary>
		public MimeEntity ParentEntity
		{
			get{ return m_pParentEntity; }
		}

		/// <summary>
		/// Gets child entities. This property is available only if ContentType = multipart/... .
		/// </summary>
		public MimeEntityCollection ChildEntities
		{
			get{ return m_pChildEntities; }
		}


		/// <summary>
		/// Gets or sets header field "<b>Mime-Version:</b>" value. Returns null if value isn't set.
		/// </summary>
		public string MimeVersion
		{
			get{
				if(m_pHeader.Contains("Mime-Version:")){
					return m_pHeader.GetFirst("Mime-Version:").Value;
				}
				else{
					return null;
				}
			}

			set{ 
				if(m_pHeader.Contains("Mime-Version:")){
					m_pHeader.GetFirst("Mime-Version:").Value = value;
				}
				else{
					m_pHeader.Add("Mime-Version:",value);
				}
			}
		}

		/// <summary>
		/// Gets or sets header field "<b>Content-Type:</b>" value. This property specifies what entity data is.
		/// NOTE: ContentType can't be changed while there is data specified(Exception is thrown) in this mime entity, because it isn't
		/// possible todo data conversion between different types. For example text/xx has charset parameter and other types don't,
		/// changing loses it and text data becomes useless.
		/// </summary>
		public MediaType_enum ContentType
		{
			get{ 
				if(m_pHeader.Contains("Content-Type:")){
					string contentType = new ParametizedHeaderField(m_pHeader.GetFirst("Content-Type:")).Value;
					return MimeUtils.ParseMediaType(contentType);
				}
				else{
					return MediaType_enum.NotSpecified;
				}				
			}

			set{
				if(this.DataEncoded != null){
					throw new Exception("ContentType can't be changed while there is data specified, set data to null before !");
				}
				if(value == MediaType_enum.Unknown){
					throw new Exception("MediaType_enum.Unkown isn't allowed to set !");
				}
				if(value == MediaType_enum.NotSpecified){
					throw new Exception("MediaType_enum.NotSpecified isn't allowed to set !");
				}
				
				string contentType = "";
				//--- Text/xxx --------------------------------//
				if(value == MediaType_enum.Text_plain){
					contentType = "text/plain; charset=\"utf-8\"";
				}
				else if(value == MediaType_enum.Text_html){
					contentType = "text/html; charset=\"utf-8\"";
				}
				else if(value == MediaType_enum.Text_xml){
					contentType = "text/xml; charset=\"utf-8\"";
				}
				else if(value == MediaType_enum.Text_rtf){
					contentType = "text/rtf; charset=\"utf-8\"";
				}
				else if(value == MediaType_enum.Text){
					contentType = "text; charset=\"utf-8\"";
				}
				//---------------------------------------------//

				//--- Image/xxx -------------------------------//
				else if(value == MediaType_enum.Image_gif){
					contentType = "image/gif";
				}
				else if(value == MediaType_enum.Image_tiff){
					contentType = "image/tiff";
				}
				else if(value == MediaType_enum.Image_jpeg){
					contentType = "image/jpeg";
				}
				else if(value == MediaType_enum.Image){
					contentType = "image";
				}
				//---------------------------------------------//

				//--- Audio/xxx -------------------------------//
				else if(value == MediaType_enum.Audio){
					contentType = "audio";
				}
				//---------------------------------------------//

				//--- Video/xxx -------------------------------//
				else if(value == MediaType_enum.Video){
					contentType = "video";
				}
				//---------------------------------------------//

				//--- Application/xxx -------------------------//
				else if(value == MediaType_enum.Application_octet_stream){
					contentType = "application/octet-stream";
				}
				else if(value == MediaType_enum.Application){
					contentType = "application";
				}
				//---------------------------------------------//

				//--- Multipart/xxx ---------------------------//
				else if(value == MediaType_enum.Multipart_mixed){
					contentType = "multipart/mixed;	boundary=\"part_" + Guid.NewGuid().ToString().Replace("-","_") + "\"";
				}
				else if(value == MediaType_enum.Multipart_alternative){
					contentType = "multipart/alternative;	boundary=\"part_" + Guid.NewGuid().ToString().Replace("-","_") + "\"";
				}
				else if(value == MediaType_enum.Multipart_parallel){
					contentType = "multipart/parallel;	boundary=\"part_" + Guid.NewGuid().ToString().Replace("-","_") + "\"";
				}
				else if(value == MediaType_enum.Multipart_related){
					contentType = "multipart/related;	boundary=\"part_" + Guid.NewGuid().ToString().Replace("-","_") + "\"";
				}
				else if(value == MediaType_enum.Multipart_signed){
					contentType = "multipart/signed;	boundary=\"part_" + Guid.NewGuid().ToString().Replace("-","_") + "\"";
				}
				else if(value == MediaType_enum.Multipart){
					contentType = "multipart;	boundary=\"part_" + Guid.NewGuid().ToString().Replace("-","_") + "\"";
				}
				//---------------------------------------------//

				//--- Message/xxx -----------------------------//
				else if(value == MediaType_enum.Message_rfc822){
					contentType = "message/rfc822";
				}
				else if(value == MediaType_enum.Message){
					contentType = "message";
				}
				//---------------------------------------------//

				else{
					throw new Exception("Invalid flags combination of MediaType_enum was specified !");
				}

				if(m_pHeader.Contains("Content-Type:")){
					m_pHeader.GetFirst("Content-Type:").Value = contentType;
				}
				else{
					m_pHeader.Add("Content-Type:",contentType);
				}
			}
		}

		
		/// <summary>
		/// Gets or sets header field "<b>Content-Type:</b>" value. Returns null if value isn't set. This property specifies what entity data is.
		/// This property is meant for advanced users, who needs other values what defined MediaType_enum provides.
		/// Example value: text/plain; charset="utf-8". 
		/// NOTE: ContentType can't be changed while there is data specified(Exception is thrown) in this mime entity, because it isn't
		/// possible todo data conversion between different types. For example text/xx has charset parameter and other types don't,
		/// changing loses it and text data becomes useless.
		/// </summary>
		public string ContentTypeString
		{
			get{
				if(m_pHeader.Contains("Content-Type:")){
					return m_pHeader.GetFirst("Content-Type:").Value;
				}
				else{
					return null;
				}
			}

			set{ 
				if(this.DataEncoded != null){
					throw new Exception("ContentType can't be changed while there is data specified, set data to null before !");
				}
				if(m_pHeader.Contains("Content-Type:")){
					m_pHeader.GetFirst("Content-Type:").Value = value;
				}
				else{
					m_pHeader.Add("Content-Type:",value);
				}
			}
		}

		/// <summary>
		/// Gets or sets header field "<b>Content-Transfer-Encoding:</b>" value. This property specifies how data is encoded/decoded.
		/// If you set this value, it's recommended that you use QuotedPrintable for text and Base64 for binary data.
		/// 7bit,_8bit,Binary are today obsolete (used for parsing). 
		/// </summary>
		public ContentTransferEncoding_enum ContentTransferEncoding
		{
			get{ 
				if(m_pHeader.Contains("Content-Transfer-Encoding:")){
					return MimeUtils.ParseContentTransferEncoding(m_pHeader.GetFirst("Content-Transfer-Encoding:").Value);
				}
				else{
					return ContentTransferEncoding_enum.NotSpecified;
				}
			}

			set{
				if(value == ContentTransferEncoding_enum.Unknown){
					throw new Exception("ContentTransferEncoding_enum.Unknown isn't allowed to set !");
				}
				if(value == ContentTransferEncoding_enum.NotSpecified){
					throw new Exception("ContentTransferEncoding_enum.NotSpecified isn't allowed to set !");
				}

				string encoding = MimeUtils.ContentTransferEncodingToString(value);

				// There is entity data specified and encoding changed, we need to convert existing data
				if(this.DataEncoded != null){
					ContentTransferEncoding_enum oldEncoding = this.ContentTransferEncoding;
					if(oldEncoding == ContentTransferEncoding_enum.Unknown || oldEncoding == ContentTransferEncoding_enum.NotSpecified){
						throw new Exception("Data can't be converted because old encoding '" + MimeUtils.ContentTransferEncodingToString(oldEncoding) + "' is unknown !");
					}

					this.DataEncoded = EncodeData(this.Data,value);
				}

				if(m_pHeader.Contains("Content-Transfer-Encoding:")){
					m_pHeader.GetFirst("Content-Transfer-Encoding:").Value = encoding;
				}
				else{
					m_pHeader.Add("Content-Transfer-Encoding:",encoding);
				}
			}
		}

		/// <summary>
		/// Gets or sets header field "<b>Content-Disposition:</b>" value.
		/// </summary>
		public ContentDisposition_enum ContentDisposition
		{
			get{ 
				if(m_pHeader.Contains("Content-Disposition:")){
					return MimeUtils.ParseContentDisposition(m_pHeader.GetFirst("Content-Disposition:").Value);
				}
				else{
					return ContentDisposition_enum.NotSpecified;
				}
			}

			set{
				if(value == ContentDisposition_enum.Unknown){
					throw new Exception("ContentDisposition_enum.Unknown isn't allowed to set !");
				}

				// Just remove Content-Disposition: header field if exists
				if(value == ContentDisposition_enum.NotSpecified){
					HeaderField disposition = m_pHeader.GetFirst("Content-Disposition:");
					if(disposition != null){
						m_pHeader.Remove(disposition);
					}
				}
				else{
					string disposition = MimeUtils.ContentDispositionToString(value);
					if(m_pHeader.Contains("Content-Disposition:")){
						m_pHeader.GetFirst("Content-Disposition:").Value = disposition;
					}
					else{
						m_pHeader.Add("Content-Disposition:",disposition);
					}
				}
			}
		}

		/// <summary>
		/// Gets or sets header field "<b>Content-Description:</b>" value. Returns null if value isn't set.
		/// </summary>
		public string ContentDescription
		{
			get{
				if(m_pHeader.Contains("Content-Description:")){
					return m_pHeader.GetFirst("Content-Description:").Value;
				}
				else{
					return null;
				}
			}

			set{ 
				if(m_pHeader.Contains("Content-Description:")){
					m_pHeader.GetFirst("Content-Description:").Value = value;
				}
				else{
					m_pHeader.Add("Content-Description:",value);
				}
			}
		}

		/// <summary>
		/// Gets or sets header field "<b>Content-ID:</b>" value. Returns null if value isn't set.
		/// </summary>
		public string ContentID
		{
			get{
				if(m_pHeader.Contains("Content-ID:")){
					return m_pHeader.GetFirst("Content-ID:").Value;
				}
				else{
					return null;
				}
			}

			set{ 
				if(m_pHeader.Contains("Content-ID:")){
					m_pHeader.GetFirst("Content-ID:").Value = value;
				}
				else{
					m_pHeader.Add("Content-ID:",value);
				}
			}
		}

		/// <summary>
		/// Gets or sets "<b>Content-Type:</b>" header field "<b>name</b>" parameter.
		/// Returns null if Content-Type: header field value isn't set or Content-Type: header field "<b>name</b>" parameter isn't set.
		/// <p/>
		/// Note: Content-Type must be application/xxx or exception is thrown.
		/// This property is obsolete today, it's replaced with <b>Content-Disposition: filename</b> parameter.
		/// If possible always set FileName property instead of it. 
		/// </summary>
		public string ContentType_Name
		{
			get{ 
				if(m_pHeader.Contains("Content-Type:")){
					ParametizedHeaderField contentType = new ParametizedHeaderField(m_pHeader.GetFirst("Content-Type:"));
					if(contentType.Parameters.Contains("name")){
						return contentType.Parameters["name"];
					}
					else{
						return null;
					}
				}
				else{
					return null;
				}
			}

			set{
				if(!m_pHeader.Contains("Content-Type:")){
					throw new Exception("Please specify Content-Type first !");
				}
				if((this.ContentType & MediaType_enum.Application) == 0){
					throw new Exception("Parameter name is available only for ContentType application/xxx !");
				}

				ParametizedHeaderField contentType = new ParametizedHeaderField(m_pHeader.GetFirst("Content-Type:"));
				if(contentType.Parameters.Contains("name")){
					contentType.Parameters["name"] = value;
				}
				else{
					contentType.Parameters.Add("name",value);
				}
			}
		}
        
		/// <summary>
		/// Gets or sets "<b>Content-Type:</b>" header field "<b>charset</b>" parameter.
		/// Returns null if Content-Type: header field value isn't set or Content-Type: header field "<b>charset</b>" parameter isn't set.
		/// If you don't know what charset to use then <b>utf-8</b> is recommended, most of times this is sufficient.
		/// Note: Content-Type must be text/xxx or exception is thrown.
		/// </summary>
		public string ContentType_CharSet
		{
			get{ 
				if(m_pHeader.Contains("Content-Type:")){
					ParametizedHeaderField contentType = new ParametizedHeaderField(m_pHeader.GetFirst("Content-Type:"));
					if(contentType.Parameters.Contains("charset")){
						return contentType.Parameters["charset"];
					}
					else{
						return null;
					}					
				}
				else{
					return null;
				}
			}

			set{
				if(!m_pHeader.Contains("Content-Type:")){
					throw new Exception("Please specify Content-Type first !");
				}
				if((this.ContentType & MediaType_enum.Text) == 0){
					throw new Exception("Parameter boundary is available only for ContentType text/xxx !");
				}

				// There is data specified, we need to convert it because charset changed
				if(this.DataEncoded != null){
					string currentCharSet = this.ContentType_CharSet;
                    if(currentCharSet == null){
                        currentCharSet = "ascii";
                    }
					try{
						System.Text.Encoding.GetEncoding(currentCharSet);
					}
					catch{
						throw new Exception("Data can't be converted because current charset '" + currentCharSet + "' isn't supported !");
					}
					try{
						System.Text.Encoding encoding = System.Text.Encoding.GetEncoding(value);
						this.Data = encoding.GetBytes(this.DataText);
					}
					catch{
						throw new Exception("Data can't be converted because new charset '" + value + "' isn't supported !");
					}
				}

				ParametizedHeaderField contentType = new ParametizedHeaderField(m_pHeader.GetFirst("Content-Type:"));
				if(contentType.Parameters.Contains("charset")){
					contentType.Parameters["charset"] = value;
				}
				else{
					contentType.Parameters.Add("charset",value);
				}
			}
		}

		/// <summary>
		/// Gets or sets "<b>Content-Type:</b>" header field "<b>boundary</b>" parameter.
		/// Returns null if Content-Type: header field value isn't set or Content-Type: header field "<b>boundary</b>" parameter isn't set.
		/// Note: Content-Type must be multipart/xxx or exception is thrown.
		/// </summary>
		public string ContentType_Boundary
		{
			get{ 
				if(m_pHeader.Contains("Content-Type:")){
					ParametizedHeaderField contentDisposition = new ParametizedHeaderField(m_pHeader.GetFirst("Content-Type:"));
					if(contentDisposition.Parameters.Contains("boundary")){
						return contentDisposition.Parameters["boundary"];
					}
					else{
						return null;
					}					
				}
				else{
					return null;
				}
			}

			set{
				if(!m_pHeader.Contains("Content-Type:")){
					throw new Exception("Please specify Content-Type first !");
				}
				if((this.ContentType & MediaType_enum.Multipart) == 0){
					throw new Exception("Parameter boundary is available only for ContentType multipart/xxx !");
				}

				ParametizedHeaderField contentType = new ParametizedHeaderField(m_pHeader.GetFirst("Content-Type:"));
				if(contentType.Parameters.Contains("boundary")){
					contentType.Parameters["boundary"] = value;
				}
				else{
					contentType.Parameters.Add("boundary",value);
				}
			}
		}

		/// <summary>
		/// Gets or sets "<b>Content-Disposition:</b>" header field "<b>filename</b>" parameter.
		/// Returns null if Content-Disposition: header field value isn't set or Content-Disposition: header field "<b>filename</b>" parameter isn't set.
		/// Note: Content-Disposition must be attachment or inline.
		/// </summary>
		public string ContentDisposition_FileName
		{
			get{ 
				if(m_pHeader.Contains("Content-Disposition:")){
					ParametizedHeaderField contentDisposition = new ParametizedHeaderField(m_pHeader.GetFirst("Content-Disposition:"));
					if(contentDisposition.Parameters.Contains("filename")){
						return contentDisposition.Parameters["filename"];
					}
					else{
						return null;
					}					
				}
				else{
					return null;
				}
			}

			set{
				if(!m_pHeader.Contains("Content-Disposition:")){
					throw new Exception("Please specify Content-Disposition first !");
				}

				ParametizedHeaderField contentType = new ParametizedHeaderField(m_pHeader.GetFirst("Content-Disposition:"));
				if(contentType.Parameters.Contains("filename")){
					contentType.Parameters["filename"] = value;
				}
				else{
					contentType.Parameters.Add("filename",value);
				}
			}
		}

		/// <summary>
		/// Gets or sets header field "<b>Date:</b>" value.
		/// </summary>
		public DateTime Date
		{
			get{ 
				if(m_pHeader.Contains("Date:")){
					try{
						return MimeUtils.ParseDate(m_pHeader.GetFirst("Date:").Value);
					}
					catch{
						return DateTime.MinValue;
					}
				}
				else{
					return DateTime.MinValue;
				}
			}

			set{ 
				if(m_pHeader.Contains("Date:")){
					m_pHeader.GetFirst("Date:").Value = MimeUtils.DateTimeToRfc2822(value);
				}
				else{
					m_pHeader.Add("Date:",MimeUtils.DateTimeToRfc2822(value));
				}
			}
		}

		/// <summary>
		/// Gets or sets header field "<b>Message-ID:</b>" value. Returns null if value isn't set.
		/// Syntax: '&lt;'id-left@id-right'&gt;'. Example: &lt;621bs724bfs8@jnfsjaas4263&gt;
		/// </summary>
		public string MessageID
		{
			get{
				if(m_pHeader.Contains("Message-ID:")){
					return m_pHeader.GetFirst("Message-ID:").Value;
				}
				else{
					return null;
				}
			}

			set{ 
				if(m_pHeader.Contains("Message-ID:")){
					m_pHeader.GetFirst("Message-ID:").Value = value;
				}
				else{
					m_pHeader.Add("Message-ID:",value);
				}
			}
		}

		/// <summary>
		/// Gets or sets header field "<b>To:</b>" value. Returns null if value isn't set.
		/// </summary>
		public AddressList To
		{
			get{ 
				if(m_pHeader.Contains("To:")){
					// There is already cached version, return it
					if(m_pHeaderFieldCache.Contains("To:")){
						return (AddressList)m_pHeaderFieldCache["To:"];
					}
					// These isn't cached version, we need to create it
					else{
						// Create and bound address-list to existing header field
						HeaderField field = m_pHeader.GetFirst("To:");
						AddressList list = new AddressList();
						list.Parse(field.Value);
						list.BoundedHeaderField = field;

						// Cache header field
						m_pHeaderFieldCache["To:"] = list;

						return list;
					}					
				}
				else{
					return null;
				}
			}

			set{
				// Just remove header field
				if(value == null){
					m_pHeader.Remove(m_pHeader.GetFirst("To:"));
					return;
				}

				// Release old address collection
				if(m_pHeaderFieldCache["To:"] != null){
					((AddressList)m_pHeaderFieldCache["To:"]).BoundedHeaderField = null;
				}

				// Bound address-list to To: header field. If header field doesn't exist, add it.
				HeaderField to = m_pHeader.GetFirst("To:");
				if(to == null){
					to = new HeaderField("To:",value.ToAddressListString());
					m_pHeader.Add(to);
				}
                else{
                    to.Value = value.ToAddressListString();
                }
				value.BoundedHeaderField = to;

                m_pHeaderFieldCache["To:"] = value;
			}
		}

		/// <summary>
		/// Gets or sets header field "<b>Cc:</b>" value. Returns null if value isn't set.
		/// </summary>
		public AddressList Cc
		{
			get{
				if(m_pHeader.Contains("Cc:")){
					// There is already cached version, return it
					if(m_pHeaderFieldCache.Contains("Cc:")){
						return (AddressList)m_pHeaderFieldCache["Cc:"];
					}
					// These isn't cached version, we need to create it
					else{
						// Create and bound address-list to existing header field
						HeaderField field = m_pHeader.GetFirst("Cc:");
						AddressList list = new AddressList();
						list.Parse(field.Value);
						list.BoundedHeaderField = field;

						// Cache header field
						m_pHeaderFieldCache["Cc:"] = list;

						return list;
					}					
				}
				else{
					return null;
				}
			}

			set{
				// Just remove header field
				if(value == null){
					m_pHeader.Remove(m_pHeader.GetFirst("Cc:"));
					return;
				}

				// Release old address collection
				if(m_pHeaderFieldCache["Cc:"] != null){
					((AddressList)m_pHeaderFieldCache["Cc:"]).BoundedHeaderField = null;
				}

				// Bound address-list to To: header field. If header field doesn't exist, add it.
				HeaderField cc = m_pHeader.GetFirst("Cc:");
				if(cc == null){
					cc = new HeaderField("Cc:",value.ToAddressListString());
					m_pHeader.Add(cc);
				}
                else{
                    cc.Value = value.ToAddressListString();
                }
				value.BoundedHeaderField = cc;

                m_pHeaderFieldCache["Cc:"] = value;
			}
		}

		/// <summary>
		/// Gets or sets header field "<b>Bcc:</b>" value. Returns null if value isn't set.
		/// </summary>
		public AddressList Bcc
		{
			get{ 
				if(m_pHeader.Contains("Bcc:")){
					// There is already cached version, return it
					if(m_pHeaderFieldCache.Contains("Bcc:")){
						return (AddressList)m_pHeaderFieldCache["Bcc:"];
					}
					// These isn't cached version, we need to create it
					else{
						// Create and bound address-list to existing header field
						HeaderField field = m_pHeader.GetFirst("Bcc:");
						AddressList list = new AddressList();
						list.Parse(field.Value);
						list.BoundedHeaderField = field;

						// Cache header field
						m_pHeaderFieldCache["Bcc:"] = list;

						return list;
					}					
				}
				else{
					return null;
				}
			}

			set{
				// Just remove header field
				if(value == null){
					m_pHeader.Remove(m_pHeader.GetFirst("Bcc:"));
					return;
				}

				// Release old address collection
				if(m_pHeaderFieldCache["Bcc:"] != null){
					((AddressList)m_pHeaderFieldCache["Bcc:"]).BoundedHeaderField = null;
				}

				// Bound address-list to To: header field. If header field doesn't exist, add it.
				HeaderField bcc = m_pHeader.GetFirst("Bcc:");
				if(bcc == null){
					bcc = new HeaderField("Bcc:",value.ToAddressListString());
					m_pHeader.Add(bcc);
				}
                else{
                    bcc.Value = value.ToAddressListString();
                }
				value.BoundedHeaderField = bcc;

                m_pHeaderFieldCache["Bcc:"] = value;
			}
		}

		/// <summary>
		/// Gets or sets header field "<b>From:</b>" value. Returns null if value isn't set.
		/// </summary>
		public AddressList From
		{
			get{  
				if(m_pHeader.Contains("From:")){
					// There is already cached version, return it
					if(m_pHeaderFieldCache.Contains("From:")){
						return (AddressList)m_pHeaderFieldCache["From:"];
					}
					// These isn't cached version, we need to create it
					else{
						// Create and bound address-list to existing header field
						HeaderField field = m_pHeader.GetFirst("From:");
						AddressList list = new AddressList();
						list.Parse(field.Value);
						list.BoundedHeaderField = field;

						// Cache header field
						m_pHeaderFieldCache["From:"] = list;

						return list;
					}					
				}
				else{
					return null;
				}
			}

			set{
				// Just remove header field
				if(value == null && m_pHeader.Contains("From:")){
					m_pHeader.Remove(m_pHeader.GetFirst("From:"));
					return;
				}

				// Release old address collection
				if(m_pHeaderFieldCache["From:"] != null){
					((AddressList)m_pHeaderFieldCache["From:"]).BoundedHeaderField = null;
				}

				// Bound address-list to To: header field. If header field doesn't exist, add it.
				HeaderField from = m_pHeader.GetFirst("From:");
				if(from == null){
					from = new HeaderField("From:",value.ToAddressListString());
					m_pHeader.Add(from);
				}
                else{
                    from.Value = value.ToAddressListString();
                }
				value.BoundedHeaderField = from;

                m_pHeaderFieldCache["From:"] = value;
			}
		}

		/// <summary>
		/// Gets or sets header field "<b>Sender:</b>" value. Returns null if value isn't set.
		/// </summary>
		public MailboxAddress Sender
		{
			get{  
				if(m_pHeader.Contains("Sender:")){
					return MailboxAddress.Parse(m_pHeader.GetFirst("Sender:").Value);
				}
				else{
					return null;
				}
			}

			set{ 
				if(m_pHeader.Contains("Sender:")){
					m_pHeader.GetFirst("Sender:").Value = value.ToMailboxAddressString();
				}
				else{
					m_pHeader.Add("Sender:",value.ToMailboxAddressString());
				}
			}
		}

		/// <summary>
		/// Gets or sets header field "<b>Reply-To:</b>" value. Returns null if value isn't set.
		/// </summary>
		public AddressList ReplyTo
		{
			get{ 
				if(m_pHeader.Contains("Reply-To:")){
					// There is already cached version, return it
					if(m_pHeaderFieldCache.Contains("Reply-To:")){
						return (AddressList)m_pHeaderFieldCache["Reply-To:"];
					}
					// These isn't cached version, we need to create it
					else{
						// Create and bound address-list to existing header field
						HeaderField field = m_pHeader.GetFirst("Reply-To:");
						AddressList list = new AddressList();
						list.Parse(field.Value);
						list.BoundedHeaderField = field;

						// Cache header field
						m_pHeaderFieldCache["Reply-To:"] = list;

						return list;
					}					
				}
				else{
					return null;
				}
			}

			set{
				// Just remove header field
				if(value == null && m_pHeader.Contains("Reply-To:")){
					m_pHeader.Remove(m_pHeader.GetFirst("Reply-To:"));
					return;
				}

				// Release old address collection
				if(m_pHeaderFieldCache["Reply-To:"] != null){
					((AddressList)m_pHeaderFieldCache["Reply-To:"]).BoundedHeaderField = null;
				}

				// Bound address-list to To: header field. If header field doesn't exist, add it.
				HeaderField replyTo = m_pHeader.GetFirst("Reply-To:");
				if(replyTo == null){
					replyTo = new HeaderField("Reply-To:",value.ToAddressListString());
					m_pHeader.Add(replyTo);
				}
                else{
                    replyTo.Value = value.ToAddressListString();
                }
				value.BoundedHeaderField = replyTo;

                m_pHeaderFieldCache["Reply-To:"] = value;
			}
		}

		/// <summary>
		/// Gets or sets header field "<b>In-Reply-To:</b>" value. Returns null if value isn't set.
		/// </summary>
		public string InReplyTo
		{
			get{ 
				if(m_pHeader.Contains("In-Reply-To:")){
					return m_pHeader.GetFirst("In-Reply-To:").Value;
				}
				else{
					return null;
				}
			}

			set{ 
				if(m_pHeader.Contains("In-Reply-To:")){
					m_pHeader.GetFirst("In-Reply-To:").Value = value;
				}
				else{
					m_pHeader.Add("In-Reply-To:",value);
				}
			}
		}

		/// <summary>
		/// Gets or sets header field "<b>Disposition-Notification-To:</b>" value. Returns null if value isn't set.
		/// </summary>
		public string DSN
		{
			get{ 
				if(m_pHeader.Contains("Disposition-Notification-To:")){
					return m_pHeader.GetFirst("Disposition-Notification-To:").Value;
				}
				else{
					return null;
				}
			}

			set{ 
				if(m_pHeader.Contains("Disposition-Notification-To:")){
					m_pHeader.GetFirst("Disposition-Notification-To:").Value = value;
				}
				else{
					m_pHeader.Add("Disposition-Notification-To:",value);
				}
			}
		}

		/// <summary>
		/// Gets or sets header field "<b>Subject:</b>" value. Returns null if value isn't set.
		/// </summary>
		public string Subject
		{
			get{ 
				if(m_pHeader.Contains("Subject:")){
					return m_pHeader.GetFirst("Subject:").Value;
				}
				else{
					return null;
				}
			}

			set{ 
				if(m_pHeader.Contains("Subject:")){
					m_pHeader.GetFirst("Subject:").Value = value;
				}
				else{
					m_pHeader.Add("Subject:",value);
				}
			}
		}


		/// <summary>
		/// Gets or sets entity data. Data is encoded/decoded with "<b>Content-Transfer-Encoding:</b>" header field value.
		/// Note: This property can be set only if Content-Type: isn't multipart.
		/// </summary>
		public byte[] Data
		{
			get{ 
				// Decode Data
				ContentTransferEncoding_enum encoding = this.ContentTransferEncoding;
				if(encoding == ContentTransferEncoding_enum.Base64){
					return Core.Base64Decode(this.DataEncoded);				
				}
				else if(encoding == ContentTransferEncoding_enum.QuotedPrintable){
					return Core.QuotedPrintableDecode(this.DataEncoded);
				}
				else{
					return this.DataEncoded;
				}
			}

			set{
				if(value == null){
					this.DataEncoded = null;
					return;
				}
				
				ContentTransferEncoding_enum encoding = this.ContentTransferEncoding;
				this.DataEncoded = EncodeData(value,encoding);
			}
		}

		/// <summary>
		/// Gets or sets entity text data. Data is encoded/decoded with "<b>Content-Transfer-Encoding:</b>" header field value with this.Charset charset.
		/// Note: This property is available only if ContentType is Text/xxx... or no content type specified, othwerwise Excpetion is thrown.
		/// </summary>
		public string DataText
		{			
			get{ 
				if((this.ContentType & MediaType_enum.Text) == 0 && (this.ContentType & MediaType_enum.NotSpecified) == 0){
					throw new Exception("This property is available only if ContentType is Text/xxx... !");
				}

				try{
					string charSet = this.ContentType_CharSet;
					// Charset isn't specified, use system default
					if(charSet == null){
						return System.Text.Encoding.Default.GetString(this.Data);
					}
					else{
						return System.Text.Encoding.GetEncoding(charSet).GetString(this.Data);
					}				
				}
				// Not supported charset, use default
				catch{
					return System.Text.Encoding.Default.GetString(this.Data);
				}
			}

			set{
				if(value == null){
					this.DataEncoded = null;
					return;
				}

				// Check charset
				string charSet = this.ContentType_CharSet;
				if(charSet == null){
					throw new Exception("Please specify CharSet property first !");
				}
                               
                Encoding encoding = null;
				try{
                    encoding = Encoding.GetEncoding(charSet);					
				}
				catch{
					throw new Exception("Not supported charset '" + charSet + "' ! If you need to use this charset, then set data through Data or DataEncoded property.");
				}
                this.Data = encoding.GetBytes(value);
			}
		}

		/// <summary>
		/// Gets or sets entity encoded data. If you set this value, be sure that you encode this value as specified by Content-Transfer-Encoding: header field.
		/// Set this value only if you need custom Content-Transfer-Encoding: what current Mime class won't support, other wise set data through this.Data property. 
		/// Note: This property can be set only if Content-Type: isn't multipart.
		/// </summary>
		public byte[] DataEncoded
		{
			get{ return m_EncodedData; }

			set{ m_EncodedData = value; }
		}
				
		#endregion

	}
}
